﻿using System;
using Tao.OpenGl;
using Tao.FreeType;
using System.Runtime.InteropServices;

namespace Aeon.Framework
{

/// <summary>
/// Font wrapper class for displaying FreeType in OpenGL.
/// 
/// </summary>
internal sealed class AeonFont : IFont
    {
    // --- Inner:
    
    private class AeonNoFont : IFont
        {
        private FontType style;
        
        public AeonNoFont()
            {
            
            style = new FontType("None",0,AeonColors.Default);
            }

        public FontType Style { get { return(style); } }

        public void BeginFont() { }
        public void Write(String text) { }
        public void EndFont() { }
        public void Render() { }
        public void Dispose() { }
        }

    /// <summary>
    /// Glyph offset information.
    /// 
    /// </summary>
    private struct GlyphOffset
        {
        public Int32 top;                    // Base-top glyph distance. Positive is upward.
        public Int32 left;                   // Left distance pixels.
        public Int32 width;                  // Glyph width in pixels.
        public Int32 height;                 // Glyph height in pixels. 
        
        public FT_Vector advance;            // Glyph advance width.
        
        public Int64 lsb_delta;              // Hinted and unhinted left bearing difference.
        public Int64 rsb_delta;              // Hinted and unhinted right bearing difference.
        
        public Int64 linearHoriAdvance;      // Glyph advance width. For outline glyphs.
        public Int64 linearVertAdvance;      // Glyph advance height. For outline glyphs.
        }

    /// <summary>
    /// Alignment of output text.
    /// [TODO] Expose this on FontType.
    /// 
    /// </summary>
    private enum Alignment
        {
        FT_ALIGN_LEFT,      // Left-align the text.
        FT_ALIGN_RIGHT,     // Right-align the text.
        FT_ALIGN_CENTERED   // Center-align the text.
        }

    // --- Attributes:
    
    private static IFont none = new AeonNoFont();       // Empty font.
    
    private static IntPtr library = IntPtr.Zero;        // FreeType library pointer.
    private static UInt16 instances = 0;                // Freetype pointer instances.
    
    private Boolean rendered;                           // Context render flag.
    
    private Alignment align;                            // Text alignment.
    private FontType style;                             // Font style.

    private IntPtr pointer;                             // Font face pointer.
    private FT_FaceRec root;                            // Font face root.
    
    private Int32 list;                                 // List base.
    private Int32[] widths;                             // Characters X space.
    private Int32[] textures;                           // Characters textures.
    private GlyphOffset[] offsets;                      // Characters offsets.

    // --- Properties:
    
    public FontType Style { get { return(style); } }
    
    public static IFont None { get { return(none); } }

    // --- Constructor:

    internal AeonFont(FontType font)
        {
        style = font;
        rendered = false;
        pointer = IntPtr.Zero;
        align = Alignment.FT_ALIGN_LEFT; 
        
        initializeLibrary();
        createFace();
        }

    // --- Public Methods:

    /// <summary>
    /// Prepare OpenGl states for text output.
    /// 
    /// </summary>
    public void BeginFont()
        {
        Gl.glPushAttrib(Gl.GL_LIST_BIT | Gl.GL_CURRENT_BIT | Gl.GL_ENABLE_BIT | Gl.GL_TRANSFORM_BIT);
        Gl.glBlendFunc(Gl.GL_SRC_ALPHA, Gl.GL_ONE_MINUS_SRC_ALPHA);

        Gl.glDisable(Gl.GL_DEPTH_TEST);
        Gl.glDisable(Gl.GL_LIGHTING);

        Gl.glEnable(Gl.GL_BLEND);
        Gl.glEnable(Gl.GL_TEXTURE_2D);

        Gl.glListBase(list);
        }

    /// <summary>
    /// Display text.
    /// Needs an active OpenGL context.
    ///     
    /// </summary>
    public void Write(String text)
        {
        Gl.glPushMatrix();

        switch (align)
            {
            case Alignment.FT_ALIGN_CENTERED: Gl.glTranslatef(-calculateWidth(text)/2,0,0); break;
            case Alignment.FT_ALIGN_RIGHT:    Gl.glTranslatef(-calculateWidth(text),0,0);   break;
            case Alignment.FT_ALIGN_LEFT:     /* Default is left aligned. No changes. */    break;
            }

        Int32 length = text.Length;            
        Byte[] bytes = new Byte[length];
        
        for (int cnt=0; cnt<length; cnt++) { bytes[cnt] = (Byte)text[cnt]; }

        Gl.glCallLists(length,Gl.GL_UNSIGNED_BYTE,bytes);
        Gl.glPopMatrix();
        }
    
    /// <summary>
    /// Restore previous OpenGL state.
    /// 
    /// </summary>
    public void EndFont()
        {
        
        Gl.glPopAttrib();
        }

    /// <summary>
    /// Guarantees rendering of each letter to a textured font.
    /// Needs an active OpenGL context.
    /// 
    /// </summary>
    public void Render()
        {
        if (!rendered)
            {
            root = (FT_FaceRec)Marshal.PtrToStructure(pointer, typeof(FT_FaceRec));

            // Size*64 for Freetype 1/64 pixel accuracy:
            FT.FT_Set_Char_Size(pointer,style.Size<<6,style.Size<<6,style.DPI,style.DPI);
            FT.FT_Set_Pixel_Sizes(pointer,style.Size,style.Size);

            offsets = new GlyphOffset[128];
            textures = new Int32[128];
            widths = new Int32[128];
            
            list = Gl.glGenLists(128);
            Gl.glGenTextures(128,textures);     // [TODO] Does this work for multiple face textures on the same index?

            for (int cha=0;cha<=127;cha++) { createCharacter(root,style.Size,cha); }
            
            rendered = true;
            }
        }

    /// <summary>
    /// Disposes this face resources.
    /// 
    /// </summary>
    public void Dispose()
        {
        if (list > 0) Gl.glDeleteLists(list, 128);
        if (textures != null) Gl.glDeleteTextures(128, textures);

        textures = null;
        widths = null;
        
        disposeFace();
        disposeLibrary();
        }

    // --- Private Methods:
    
    /// <summary>
    /// Creates texture for one character.
    /// 
    /// </summary>
    private void createCharacter(FT_FaceRec face, UInt16 siz, int cha)
        {
        GlyphOffset offset = new GlyphOffset();

        //We first convert the number index to a character index
        uint index = FT.FT_Get_Char_Index(pointer, (uint)cha);

        //Here we load the actual glyph for the character
        int ret = FT.FT_Load_Glyph(pointer, index, FT.FT_LOAD_DEFAULT);
        
        FT_GlyphSlotRec glyphrec = (FT_GlyphSlotRec)Marshal.PtrToStructure(face.glyph, typeof(FT_GlyphSlotRec));

        ret = FT.FT_Render_Glyph(ref glyphrec, FT_Render_Mode.FT_RENDER_MODE_NORMAL);

        int size = (glyphrec.bitmap.width * glyphrec.bitmap.rows);
        if (size <= 0)
            {
            //Console.Write("Blank Character: " + c.ToString());
            //space is a special `blank` character
            widths[cha] = 0;
            if (cha == 32)
                {
                Gl.glNewList((list + cha), Gl.GL_COMPILE);
                Gl.glTranslatef(siz >> 1, 0, 0);
                widths[cha] = siz >> 1;
                Gl.glEndList();
                offset.left = 0;
                offset.top = 0;
                offset.height = 0;
                offset.width = widths[cha];
                offsets[cha] = offset;
                }

            return;
            }

        byte[] bmp = new byte[size];
        Marshal.Copy(glyphrec.bitmap.buffer, bmp, 0, bmp.Length);

        //Next we expand the bitmap into an opengl texture              
        int width = nextPosition(glyphrec.bitmap.width);
        int height = nextPosition(glyphrec.bitmap.rows);

        byte[] expanded = new byte[2 * width * height];
        for (int j = 0; j < height; j++)
            {
            for (int i = 0; i < width; i++)
                {
                //Luminance
                expanded[2 * (i + j * width)] = (byte)255;
                //expanded[4 * (i + j * width) + 1] = (byte)255;
                //expanded[4 * (i + j * width) + 2] = (byte)255;

                // Alpha
                expanded[2 * (i + j * width) + 1] =
                        (i >= glyphrec.bitmap.width || j >= glyphrec.bitmap.rows) ?
                        (byte)0 : (byte)(bmp[i + glyphrec.bitmap.width * j]);
                }
            }

        //Set up some texture parameters for opengl
        Gl.glBindTexture(Gl.GL_TEXTURE_2D, textures[cha]);
        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_WRAP_S, Gl.GL_CLAMP_TO_EDGE);
        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_WRAP_T, Gl.GL_CLAMP_TO_EDGE);
        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MAG_FILTER, Gl.GL_LINEAR);
        Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MIN_FILTER, Gl.GL_LINEAR);

        //Create the texture
        Gl.glTexImage2D(Gl.GL_TEXTURE_2D, 0, Gl.GL_RGBA, width, height, 0, Gl.GL_LUMINANCE_ALPHA, Gl.GL_UNSIGNED_BYTE, expanded);
        expanded = null;
        bmp = null;

        //Create a display list and bind a texture to it
        Gl.glNewList((list + cha), Gl.GL_COMPILE);
        Gl.glBindTexture(Gl.GL_TEXTURE_2D, textures[cha]);

        //Account for freetype spacing rules
        Gl.glTranslatef(glyphrec.bitmap_left, 0, 0);
        Gl.glPushMatrix();
        Gl.glTranslatef(0, glyphrec.bitmap_top - glyphrec.bitmap.rows, 0);
        float x = (float)glyphrec.bitmap.width / (float)width;
        float y = (float)glyphrec.bitmap.rows / (float)height;

        offset.left = glyphrec.bitmap_left;
        offset.top = glyphrec.bitmap_top;
        offset.height = glyphrec.bitmap.rows;
        offset.width = glyphrec.bitmap.width;
        offset.advance = glyphrec.advance;
        offset.lsb_delta = glyphrec.lsb_delta;
        offset.rsb_delta = glyphrec.rsb_delta;
        offset.linearHoriAdvance = glyphrec.linearHoriAdvance;
        offset.linearVertAdvance = glyphrec.linearVertAdvance;
        offsets[cha] = offset;

        //Draw the quad
        Gl.glBegin(Gl.GL_QUADS);
        Gl.glTexCoord2d(0, 0); Gl.glVertex2f(0, glyphrec.bitmap.rows);
        Gl.glTexCoord2d(0, y); Gl.glVertex2f(0, 0);
        Gl.glTexCoord2d(x, y); Gl.glVertex2f(glyphrec.bitmap.width, 0);
        Gl.glTexCoord2d(x, 0); Gl.glVertex2f(glyphrec.bitmap.width, glyphrec.bitmap.rows);
        Gl.glEnd();
        Gl.glPopMatrix();

        //Advance for the next character                        
        Gl.glTranslatef(glyphrec.bitmap.width, 0, 0);
        widths[cha] = glyphrec.bitmap_left + glyphrec.bitmap.width;
        Gl.glEndList();
        }

    /// <summary>
    /// Closest next power of two from given number.
    /// 
    /// </summary>
    private int nextPosition(int pos)
        {
        int rval = 1;
        while (rval < pos) rval <<= 1;
        return rval;
        }

    /// <summary>
    /// Returns the string width in pixels.
    /// 
    /// </summary>
    private Int32 calculateWidth(String txt)
        {
        Int32 wdt = 0;
        Int32 len = txt.Length;

        for (int cha=0; cha<len; cha++) wdt += widths[txt[cha]];
        return(wdt);
        }
        
    // --- Freetype Library:
    
    /// <summary>
    /// Initialize the FreeType library.
    /// 
    /// </summary>
    private void initializeLibrary()
        {
        if (library == IntPtr.Zero)
            {         
            try { FT.FT_Init_FreeType(out library); }        
            catch { throw new Exception("Couldn't bind FreeType library."); }
            }
        }
   
    /// <summary>
    /// Dispose of the FreeType library.
    /// 
    /// </summary>
    private void disposeLibrary()
        {
        if (library != IntPtr.Zero && instances == 0)
            {
            try { FT.FT_Done_FreeType(library); library = IntPtr.Zero; }
            catch { throw new Exception("Couldn't release FreeType library."); }
            }
        }
             
    /// <summary>
    /// Creates a FreeType face pointer.
    /// 
    /// </summary>    
    private void createFace()
        {
        if (pointer == IntPtr.Zero)
            {
            int success = FT.FT_New_Face(library, style.Face, 0, out pointer);        // [TODO] This hardcoded zero remains suspicious.
        
            if (success==0) { instances++; }
            else { throw new Exception("Failed to create font " + style.Face + "."); }
            }
        }
    
    /// <summary>
    /// Releases a Freetype face pointer.
    /// 
    /// </summary>    
    private void disposeFace()
        {
        if (pointer != IntPtr.Zero)
            {
            int success = FT.FT_Done_Face(pointer);
        
            if (success==0) { pointer = IntPtr.Zero; instances--; }
            else { throw new Exception("Failed to dispose font " + style.Face + "."); }
            }
        }

    }


}
